﻿#region License
/* ---------------------------------------------------------------- *
 *
 * This file is part of the Xcoordination Application Space
 * ("XcoAppSpace") http://www.xcoordination.com
 *
 * Copyright (C) 2009 Xcoordination http://www.xcoordination.com
 *
 * XcoAppSpace is free software; you can redistribute it and/or
 * modify it under the terms of version 2 of the GNU General
 * Public License as published by the Free Software Foundation.
 *
 * XcoAppSpace is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see http://www.gnu.org/licenses/
 * or write to the Free Software Foundation, Inc., 51 Franklin
 * Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 * ---------------------------------------------------------------- */
#endregion

using System;
using System.Threading;
using Microsoft.Ccr.Core;
using XcoAppSpaces.Contracts.CcrExtensions;
using XcoAppSpaces.Contracts.Exceptions;
using XcoAppSpaces.Contracts.Messaging;

namespace XcoAppSpaces.Core
{
	/// <summary>
	/// Extensions for the IPort and Port{T} classes.
	/// </summary>
    public static class PortExtensions
	{
		//DispatcherQueue for the PostWithCausality and Send implementations. This is needed
		//because the extension method doesn't have access to the AppSpace DispatcherQueues
		private static readonly DispatcherQueue dispatcherQueue = new DispatcherQueue();

		/// <summary>
		/// Get information about the port concerning remote communication.
		/// </summary>
		/// <param name="port">The port.</param>
		/// <returns>Remote communication information about the port.</returns>
		public static XcoPortInfo GetPortInfo(this IPort port)
		{
			return PortHelpers.GetPortInfo(port);
		}

		#region TestSync
		/// <summary>
		/// Get a message from the port. If the port is currently empty wait for the next message.
		/// </summary>
		/// <typeparam name="TPortElement">The message type.</typeparam>
		/// <param name="port">The port.</param>
		/// <returns>The next message in the port.</returns>
        public static TPortElement TestSync<TPortElement>(this Port<TPortElement> port)
        {
            return TestSync(port, Timeout.Infinite);
        }

		/// <summary>
		/// Get a message from the port. If the port is currently empty wait for the next message 
		/// until the given timeout. An exception is thrown if no message arrives in time.
		/// </summary>
		/// <typeparam name="TPortElement">The message type.</typeparam>
		/// <param name="port">The port.</param>
		/// <param name="timeout">The maximum time to wait.</param>
		/// <returns>The next message in the port.</returns>
        public static TPortElement TestSync<TPortElement>(this Port<TPortElement> port, int timeout)
        {
            using (var dpq = new DispatcherQueue())
            {
                TPortElement elementArrived = default(TPortElement);
                var elementArrivedSentinel = new AutoResetEvent(false);

                var oneOffReceiver = port.Receive(
                                            element =>
                                                {
                                                    elementArrived = element;
                                                    elementArrivedSentinel.Set();
                                                });
                Arbiter.Activate(
                    dpq,
                    oneOffReceiver
                    );

                if (!elementArrivedSentinel.WaitOne(timeout))
                {
                    oneOffReceiver.Cleanup();
                    throw new TimeoutException("No message has arrived on port until timeout!");
                }

                return elementArrived;
            }
		}
		#endregion

		#region PostWithCausality
		/// <summary>
		/// Posts a message to the port while using the given exceptionPort as causality.
		/// </summary>
		/// <typeparam name="TPortElement">The message type.</typeparam>
		/// <param name="port">The port.</param>
		/// <param name="message">The message to be posted.</param>
		/// <param name="exceptionPort">The port where exceptions should be returned.</param>
        public static void PostWithCausality<TPortElement>(this Port<TPortElement> port, TPortElement message, Port<Exception> exceptionPort)
        {
            port.PostWithCausality(message, exceptionPort, null);
        }

		/// <summary>
		/// Posts a message to the port while using the given exceptionHandler register to a causality 
		/// for returning exceptions.
		/// </summary>
		/// <typeparam name="TPortElement">The message type.</typeparam>
		/// <param name="port">The port.</param>
		/// <param name="message">The message to be posted.</param>
		/// <param name="exceptionHandler">The handler where exceptions should be returned.</param>
        public static void PostWithCausality<TPortElement>(this Port<TPortElement> port, TPortElement message, Handler<Exception> exceptionHandler)
        {
            port.PostWithCausality(message, exceptionHandler, null);
        }

		/// <summary>
		/// Posts a message to the port while using the given exceptionHandler register to a causality 
		/// for returning exceptions.
		/// </summary>
		/// <typeparam name="TPortElement">The message type.</typeparam>
		/// <param name="port">The port.</param>
		/// <param name="message">The message to be posted.</param>
		/// <param name="exceptionHandler">The handler where exceptions should be returned.</param>
		/// <param name="coordinationPort">The port which should be registered in the causality as coordination port.</param>
        public static void PostWithCausality<TPortElement>(this Port<TPortElement> port, TPortElement message, Handler<Exception> exceptionHandler, IPort coordinationPort)
        {
            var exceptionPort = new Port<Exception>();
            Arbiter.Activate(
                dispatcherQueue,
                exceptionPort.Receive(exceptionHandler)
                );

            port.PostWithCausality(message, exceptionPort, coordinationPort);
        }

		/// <summary>
		/// Posts a message to the port while using the given exceptionPort as causality.
		/// </summary>
		/// <typeparam name="TPortElement">The message type.</typeparam>
		/// <param name="port">The port.</param>
		/// <param name="message">The message to be posted.</param>
		/// <param name="exceptionPort">The port where exceptions should be returned.</param>
		/// <param name="coordinationPort">The port which should be registered in the causality as coordination port.</param>
        public static void PostWithCausality<TPortElement>(this Port<TPortElement> port, TPortElement message, Port<Exception> exceptionPort, IPort coordinationPort)
        {
            var closure = new Causality("default", exceptionPort, coordinationPort);
            Dispatcher.AddCausality(closure);
            port.Post(message);
            Dispatcher.RemoveCausality(closure);
		}
		#endregion

		#region Send and SendWithCausality
		/// <summary>
		/// If the port is a remote port, this method sends the given item synchronously, meaning the method only 
		/// returns as soon as the item has been transferred successfully. This allows directly catching communication 
		/// errors, and by that ensuring that after this method the item has been transferred correctly.
		/// If the port is a local port, the item will simply be posted as normal.
		/// </summary>
		/// <typeparam name="TPortElement">The message type.</typeparam>
		/// <param name="port">The port.</param>
		/// <param name="item">The item to be sent.</param>
		public static void Send<TPortElement>(this Port<TPortElement> port, TPortElement item)
		{
			port.SendWithCausality(item, (Port<Exception>)null);
		}

		/// <summary>
		/// If the port is a remote port, this method sends the given item synchronously, meaning the method only 
		/// returns as soon as the item has been transferred successfully. This allows directly catching communication 
		/// errors, and by that ensuring that after this method the item has been transferred correctly.
		/// If the port is a local port, the item will simply be posted as normal.
		/// </summary>
		/// <param name="port">The port.</param>
		/// <param name="item">The item to be sent.</param>
		public static void SendUnknownType(this IPort port, object item)
		{
			var remotableTask = port.GetRemotableTask();
			if (remotableTask == null) //if port it no remote port then just do a normal post
				port.PostUnknownType(item);
			else
				SendAndWaitForAckOrError(remotableTask, item, null);
		}

		/// <summary>
		/// If the port is a remote port, this method sends the given item synchronously, meaning the method only 
		/// returns as soon as the item has been transferred successfully. This allows directly catching communication 
		/// errors, and by that ensuring that after this method the item has been transferred correctly.
		/// If the port is a local port, the item will simply be posted as normal.
		/// The given exceptionPort is used as causality.
		/// </summary>
		/// <typeparam name="TPortElement">The message type.</typeparam>
		/// <param name="port">The port.</param>
		/// <param name="message">The message to be sent.</param>
		/// <param name="exceptionPort">The port where exceptions should be returned.</param>
		public static void SendWithCausality<TPortElement>(this Port<TPortElement> port, TPortElement message, Port<Exception> exceptionPort)
		{
			var remotableTask = port.GetRemotableTask();
			if (remotableTask == null) //if port it no remote port then just do a normal post
				Post(port, message, exceptionPort);
			else
				SendAndWaitForAckOrError(remotableTask, message, exceptionPort);
		}

		/// <summary>
		/// If the port is a remote port, this method sends the given item synchronously, meaning the method only 
		/// returns as soon as the item has been transferred successfully. This allows directly catching communication 
		/// errors, and by that ensuring that after this method the item has been transferred correctly.
		/// If the port is a local port, the item will simply be posted as normal.
		/// The given exceptionHandler is used to register to a causality  for returning exceptions.
		/// </summary>
		/// <typeparam name="TPortElement">The message type.</typeparam>
		/// <param name="port">The port.</param>
		/// <param name="message">The message to be sent.</param>
		/// <param name="exceptionHandler">The handler where exceptions should be returned.</param>
		public static void SendWithCausality<TPortElement>(this Port<TPortElement> port, TPortElement message, Handler<Exception> exceptionHandler)
		{
			var exceptionPort = new Port<Exception>();
			Arbiter.Activate(dispatcherQueue,
				exceptionPort.Receive(exceptionHandler));

			port.SendWithCausality(message, exceptionPort);
		}

		private static void Post<T>(Port<T> port, T message, Port<Exception> exceptionPort)
		{
			if (exceptionPort != null)
				port.Post(message);
			else
				port.PostWithCausality(message, exceptionPort);
		}

		private static void SendAndWaitForAckOrError(IRemotableTask remotableTask, object message, Port<Exception> causalityExceptionPort)
		{
			//set up ports for ack and error
			var commErrorPort = new Port<Exception>();
			var ackPort = new Port<Ack>();
			ManualResetEvent evt = new ManualResetEvent(false);
			Exception error = null;
			Arbiter.Activate(dispatcherQueue, Arbiter.Choice(
				commErrorPort.Receive(e => { error = e; evt.Set(); }),
				ackPort.Receive(_ => evt.Set())
				));

			//install causality if needed
			Causality closure = null;
			if (causalityExceptionPort != null)
			{
				closure = new Causality("default", causalityExceptionPort);
				Dispatcher.AddCausality(closure);
			}

			//send message
			remotableTask.Send(message, commErrorPort, ackPort);
			
			if (closure != null)
				Dispatcher.RemoveCausality(closure);

			//wait for ack or error
			evt.WaitOne();
			if (error != null)
				throw new XcoCommunicationException("Error sending message: " + error.Message, error);
		}
		#endregion
	}
}
